Une exploration approfondie du helper d'itérateur asynchrone JavaScript 'scan', ses fonctionnalités, cas d'usage et avantages pour le traitement accumulatif asynchrone.
Helper d'Itérateur Asynchrone JavaScript : Scan - Traitement Accumulatif Asynchrone
La programmation asynchrone est une pierre angulaire du dĂ©veloppement JavaScript moderne, en particulier lorsqu'il s'agit d'opĂ©rations liĂ©es aux I/O, telles que les requĂȘtes rĂ©seau ou les interactions avec le systĂšme de fichiers. Les itĂ©rateurs asynchrones, introduits dans ES2018, fournissent un mĂ©canisme puissant pour gĂ©rer les flux de donnĂ©es asynchrones. Le helper `scan`, souvent prĂ©sent dans des bibliothĂšques comme RxJS et de plus en plus disponible en tant qu'utilitaire autonome, libĂšre encore plus de potentiel pour le traitement de ces flux de donnĂ©es asynchrones.
Comprendre les Itérateurs Asynchrones
Avant de plonger dans `scan`, rappelons ce que sont les itĂ©rateurs asynchrones. Un itĂ©rateur asynchrone est un objet qui se conforme au protocole de l'itĂ©rateur asynchrone. Ce protocole dĂ©finit une mĂ©thode `next()` qui retourne une promesse se rĂ©solvant en un objet avec deux propriĂ©tĂ©s : `value` (la prochaine valeur de la sĂ©quence) et `done` (un boolĂ©en indiquant si l'itĂ©rateur est terminĂ©). Les itĂ©rateurs asynchrones sont particuliĂšrement utiles lorsque l'on travaille avec des donnĂ©es qui arrivent au fil du temps, ou des donnĂ©es qui nĂ©cessitent des opĂ©rations asynchrones pour ĂȘtre rĂ©cupĂ©rĂ©es.
Voici un exemple de base d'un itérateur asynchrone :
async function* generateNumbers() {
yield 1;
yield 2;
yield 3;
}
async function main() {
const iterator = generateNumbers();
let result = await iterator.next();
console.log(result); // { value: 1, done: false }
result = await iterator.next();
console.log(result); // { value: 2, done: false }
result = await iterator.next();
console.log(result); // { value: 3, done: false }
result = await iterator.next();
console.log(result); // { value: undefined, done: true }
}
main();
Introduction au Helper `scan`
Le helper `scan` (également connu sous le nom de `accumulate` ou `reduce`) transforme un itérateur asynchrone en appliquant une fonction accumulateur à chaque valeur et en émettant le résultat accumulé. C'est analogue à la méthode `reduce` sur les tableaux, mais fonctionne de maniÚre asynchrone et sur des itérateurs.
Essentiellement, `scan` prend un itérateur asynchrone, une fonction accumulateur et une valeur initiale facultative. Pour chaque valeur émise par l'itérateur source, la fonction accumulateur est appelée avec la valeur accumulée précédente (ou la valeur initiale s'il s'agit de la premiÚre itération) et la valeur actuelle de l'itérateur. Le résultat de la fonction accumulateur devient la nouvelle valeur accumulée, qui est ensuite émise par l'itérateur asynchrone résultant.
Syntaxe et ParamĂštres
La syntaxe générale pour utiliser `scan` est la suivante :
async function* scan(sourceIterator, accumulator, initialValue) {
let accumulatedValue = initialValue;
for await (const value of sourceIterator) {
accumulatedValue = accumulator(accumulatedValue, value);
yield accumulatedValue;
}
}
- `sourceIterator` : L'itérateur asynchrone à transformer.
- `accumulator` : Une fonction qui prend deux arguments : la valeur accumulée précédente et la valeur actuelle de l'itérateur. Elle doit retourner la nouvelle valeur accumulée.
- `initialValue` (facultatif) : La valeur initiale de l'accumulateur. Si elle n'est pas fournie, la premiÚre valeur de l'itérateur source sera utilisée comme valeur initiale, et la fonction accumulateur sera appelée à partir de la deuxiÚme valeur.
Cas d'Usage et Exemples
Le helper `scan` est incroyablement polyvalent et peut ĂȘtre utilisĂ© dans un large Ă©ventail de scĂ©narios impliquant des flux de donnĂ©es asynchrones. Voici quelques exemples :
1. Calcul d'un Total Cumulé
Imaginez que vous ayez un itérateur asynchrone qui émet des montants de transactions. Vous pouvez utiliser `scan` pour calculer un total cumulé de ces transactions.
async function* generateTransactions() {
yield 10;
yield 20;
yield 30;
}
async function main() {
const transactions = generateTransactions();
const runningTotals = scan(transactions, (acc, value) => acc + value, 0);
for await (const total of runningTotals) {
console.log(total); // Sortie : 10, 30, 60
}
}
main();
Dans cet exemple, la fonction `accumulator` ajoute simplement le montant de la transaction actuelle au total précédent. La `initialValue` de 0 garantit que le total cumulé commence à zéro.
2. Accumuler des Données dans un Tableau
Vous pouvez utiliser `scan` pour accumuler des donnĂ©es d'un itĂ©rateur asynchrone dans un tableau. Cela peut ĂȘtre utile pour collecter des donnĂ©es au fil du temps et les traiter par lots.
async function* fetchData() {
yield { id: 1, name: 'Alice' };
yield { id: 2, name: 'Bob' };
yield { id: 3, name: 'Charlie' };
}
async function main() {
const dataStream = fetchData();
const accumulatedData = scan(dataStream, (acc, value) => [...acc, value], []);
for await (const data of accumulatedData) {
console.log(data); // Sortie : [{id: 1, name: 'Alice'}], [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}], [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}, {id: 3, name: 'Charlie'}]
}
}
main();
Ici, la fonction `accumulator` utilise l'opérateur de décomposition (`...`) pour créer un nouveau tableau contenant tous les éléments précédents et la valeur actuelle. La `initialValue` est un tableau vide.
3. Implémenter un Limiteur de Débit
Un cas d'usage plus complexe est l'implĂ©mentation d'un limiteur de dĂ©bit. Vous pouvez utiliser `scan` pour suivre le nombre de requĂȘtes effectuĂ©es dans une certaine fenĂȘtre de temps et retarder les requĂȘtes suivantes si la limite de dĂ©bit est dĂ©passĂ©e.
async function* generateRequests() {
// Simuler les requĂȘtes entrantes
yield Date.now();
await new Promise(resolve => setTimeout(resolve, 200));
yield Date.now();
await new Promise(resolve => setTimeout(resolve, 100));
yield Date.now();
}
async function main() {
const requests = generateRequests();
const rateLimitWindow = 1000; // 1 seconde
const maxRequestsPerWindow = 2;
async function* rateLimitedRequests(source, window, maxRequests) {
let queue = [];
for await (const requestTime of source) {
queue.push(requestTime);
queue = queue.filter(t => requestTime - t < window);
if (queue.length > maxRequests) {
const earliestRequest = queue[0];
const delay = window - (requestTime - earliestRequest);
console.log(`Limite de débit dépassée. Délai de ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
yield requestTime;
}
}
const limited = rateLimitedRequests(requests, rateLimitWindow, maxRequestsPerWindow);
for await (const requestTime of limited) {
console.log(`RequĂȘte traitĂ©e Ă ${requestTime}`);
}
}
main();
Cet exemple utilise `scan` en interne (dans la fonction `rateLimitedRequests`) pour maintenir une file d'attente d'horodatages de requĂȘtes. Il vĂ©rifie si le nombre de requĂȘtes dans la fenĂȘtre de limite de dĂ©bit dĂ©passe le maximum autorisĂ©. Si c'est le cas, il calcule le dĂ©lai nĂ©cessaire et fait une pause avant de cĂ©der la requĂȘte.
4. Construire un Agrégateur de Données en Temps Réel (Exemple Global)
ConsidĂ©rez une application financiĂšre mondiale qui doit agrĂ©ger les cours des actions en temps rĂ©el provenant de diverses bourses. Un itĂ©rateur asynchrone pourrait diffuser les mises Ă jour des prix depuis des bourses comme le New York Stock Exchange (NYSE), le London Stock Exchange (LSE) et le Tokyo Stock Exchange (TSE). `scan` peut ĂȘtre utilisĂ© pour maintenir une moyenne mobile ou le prix le plus haut/bas pour une action particuliĂšre sur toutes les bourses.
// Simuler la diffusion des cours des actions depuis différentes bourses
async function* generateStockPrices() {
yield { exchange: 'NYSE', symbol: 'AAPL', price: 170.50 };
yield { exchange: 'LSE', symbol: 'AAPL', price: 170.75 };
await new Promise(resolve => setTimeout(resolve, 50));
yield { exchange: 'TSE', symbol: 'AAPL', price: 170.60 };
}
async function main() {
const stockPrices = generateStockPrices();
// Utiliser scan pour calculer un prix moyen mobile
const runningAverages = scan(
stockPrices,
(acc, priceUpdate) => {
const { total, count } = acc;
return { total: total + priceUpdate.price, count: count + 1 };
},
{ total: 0, count: 0 }
);
for await (const averageData of runningAverages) {
const averagePrice = averageData.total / averageData.count;
console.log(`Prix moyen mobile : ${averagePrice.toFixed(2)}`);
}
}
main();
Dans cet exemple, la fonction `accumulator` calcule le total cumulé des prix et le nombre de mises à jour reçues. Le prix moyen final est ensuite calculé à partir de ces valeurs accumulées. Cela fournit une vue en temps réel du cours de l'action sur différents marchés mondiaux.
5. Analyser le Trafic d'un Site Web Ă l'Ăchelle Mondiale
Imaginez une plateforme d'analyse web mondiale qui reçoit des flux de données de visites de sites web depuis des serveurs situés dans le monde entier. Chaque point de données représente un utilisateur visitant le site web. En utilisant `scan`, nous pouvons analyser la tendance des pages vues par pays en temps réel. Disons que les données ressemblent à : `{ country: "US", page: "homepage", timestamp: 1678886400 }`.
async function* generateWebsiteVisits() {
yield { country: 'US', page: 'homepage', timestamp: Date.now() };
yield { country: 'CA', page: 'product', timestamp: Date.now() };
yield { country: 'UK', page: 'blog', timestamp: Date.now() };
yield { country: 'US', page: 'product', timestamp: Date.now() };
}
async function main() {
const visitStream = generateWebsiteVisits();
const pageViewCounts = scan(
visitStream,
(acc, visit) => {
const { country } = visit;
const newAcc = { ...acc };
newAcc[country] = (newAcc[country] || 0) + 1;
return newAcc;
},
{}
);
for await (const counts of pageViewCounts) {
console.log('Nombre de pages vues par pays :', counts);
}
}
main();
Ici, la fonction `accumulator` met à jour un compteur pour chaque pays. La sortie montrerait le décompte accumulé des pages vues pour chaque pays à mesure que de nouvelles données de visite arrivent.
Avantages de l'Utilisation de `scan`
Le helper `scan` offre plusieurs avantages lorsque l'on travaille avec des flux de données asynchrones :
- Style Déclaratif : `scan` vous permet d'exprimer une logique de traitement accumulatif de maniÚre déclarative et concise, améliorant la lisibilité et la maintenabilité du code.
- Gestion de l'Asynchronisme : Il gÚre de maniÚre transparente les opérations asynchrones au sein de la fonction accumulateur, ce qui le rend adapté aux scénarios complexes impliquant des tùches liées aux I/O.
- Traitement en Temps Réel : `scan` permet le traitement en temps réel des flux de données, vous permettant de réagir aux changements au fur et à mesure qu'ils se produisent.
- ComposabilitĂ© : Il peut ĂȘtre facilement composĂ© avec d'autres helpers d'itĂ©rateurs asynchrones pour crĂ©er des pipelines de traitement de donnĂ©es complexes.
Implémenter `scan` (S'il n'est pas Disponible)
Bien que certaines bibliothÚques fournissent un helper `scan` intégré, vous pouvez facilement implémenter le vÎtre si nécessaire. Voici une implémentation simple :
async function* scan(sourceIterator, accumulator, initialValue) {
let accumulatedValue = initialValue;
let first = true;
for await (const value of sourceIterator) {
if (first && initialValue === undefined) {
accumulatedValue = value;
first = false;
} else {
accumulatedValue = accumulator(accumulatedValue, value);
}
yield accumulatedValue;
}
}
Cette implĂ©mentation parcourt l'itĂ©rateur source et applique la fonction accumulateur Ă chaque valeur, en cĂ©dant le rĂ©sultat accumulĂ©. Elle gĂšre le cas oĂč aucune `initialValue` n'est fournie en utilisant la premiĂšre valeur de l'itĂ©rateur source comme valeur initiale.
Comparaison avec `reduce`
Il est important de distinguer `scan` de `reduce`. While both operate on iterators and use an accumulator function, they differ in their behavior and output.
- `scan` émet la valeur accumulée pour chaque itération, fournissant un historique continu de l'accumulation.
- `reduce` n'émet que la valeur accumulée finale aprÚs avoir traité tous les éléments de l'itérateur.
Par consĂ©quent, `scan` est adaptĂ© aux scĂ©narios oĂč vous avez besoin de suivre les Ă©tats intermĂ©diaires de l'accumulation, tandis que `reduce` est appropriĂ© lorsque vous n'avez besoin que du rĂ©sultat final.
Gestion des Erreurs
Lorsque vous travaillez avec des itérateurs asynchrones et `scan`, il est crucial de gérer les erreurs avec élégance. Des erreurs peuvent survenir pendant le processus d'itération ou au sein de la fonction accumulateur. Vous pouvez utiliser des blocs `try...catch` pour attraper et gérer ces erreurs.
async function* generatePotentiallyFailingData() {
yield 1;
yield 2;
throw new Error('Quelque chose s\'est mal passé !');
yield 3;
}
async function main() {
const dataStream = generatePotentiallyFailingData();
try {
const accumulatedData = scan(dataStream, (acc, value) => acc + value, 0);
for await (const data of accumulatedData) {
console.log(data);
}
} catch (error) {
console.error('Une erreur est survenue :', error);
}
}
main();
Dans cet exemple, le bloc `try...catch` attrape l'erreur lancée par l'itérateur `generatePotentiallyFailingData`. Vous pouvez alors gérer l'erreur de maniÚre appropriée, par exemple en la journalisant ou en réessayant l'opération.
Conclusion
Le helper `scan` est un outil puissant pour effectuer un traitement accumulatif asynchrone sur les itérateurs asynchrones JavaScript. Il vous permet d'exprimer des transformations de données complexes de maniÚre déclarative et concise, de gérer les opérations asynchrones avec élégance et de traiter les flux de données en temps réel. By understanding its functionality and use cases, you can leverage `scan` to build more robust and efficient asynchronous applications. Que vous calculiez des totaux cumulés, accumuliez des données dans des tableaux, implémentiez des limiteurs de débit ou construisiez des agrégateurs de données en temps réel, `scan` peut simplifier votre code et améliorer ses performances globales. N'oubliez pas de prendre en compte la gestion des erreurs et de choisir `scan` plutÎt que `reduce` lorsque vous avez besoin d'accéder aux valeurs accumulées intermédiaires pendant le traitement de vos flux de données asynchrones. L'exploration de bibliothÚques comme RxJS peut encore améliorer votre compréhension et votre application pratique de `scan` dans les paradigmes de programmation réactive.